#ifndef W_LOG_H_
#define W_LOG_H_

#include <fstream>
#include <csignal>
#include <unordered_map>


#define LOG_USE_SPDLOG


#ifdef LOG_USE_SPDLOG

// #define SPDLOG_FUNCTION __PRETTY_FUNCTION__
#define SPDLOG_LEVEL_NAMES                                        \
	{                                                             \
		"TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL", "OFF" \
	}

#include "spdlog/spdlog.h"
#include "spdlog/async.h"
#include "spdlog/fmt/bin_to_hex.h"
#include "spdlog/sinks/rotating_file_sink.h"
#include "spdlog/sinks/stdout_color_sinks.h"
#include "spdlog/sinks/daily_file_sink.h"
#include "spdlog/sinks/hourly_file_sink.h"

#define LOG_LEVEL_TRACE SPDLOG_LEVEL_TRACE
#define LOG_LEVEL_DEBUG SPDLOG_LEVEL_DEBUG
#define LOG_LEVEL_INFO SPDLOG_LEVEL_INFO
#define LOG_LEVEL_WARN SPDLOG_LEVEL_WARN
#define LOG_LEVEL_ERROR SPDLOG_LEVEL_ERROR
#define LOG_LEVEL_FATAL SPDLOG_LEVEL_CRITICAL

#define _LOGGER_CALL(logger, level, ...)                    \
	do                                                      \
	{                                                       \
		if ((logger)->should_log(level))                    \
		{                                                   \
			SPDLOG_LOGGER_CALL(logger, level, __VA_ARGS__); \
		}                                                   \
	} while (0)

#if SPDLOG_ACTIVE_LEVEL <= SPDLOG_LEVEL_TRACE
#define LOG_TRACE(...) _LOGGER_CALL(spdlog::default_logger_raw(), spdlog::level::trace, __VA_ARGS__)
#else
#define LOG_TRACE(...) (void)0
#endif

#if SPDLOG_ACTIVE_LEVEL <= SPDLOG_LEVEL_DEBUG
#define LOG_DEBUG(...) _LOGGER_CALL(spdlog::default_logger_raw(), spdlog::level::debug, __VA_ARGS__)
#else
#define LOG_DEBUG(...) (void)0
#endif

#if SPDLOG_ACTIVE_LEVEL <= SPDLOG_LEVEL_INFO
#define LOG_INFO(...) _LOGGER_CALL(spdlog::default_logger_raw(), spdlog::level::info, __VA_ARGS__)
#else
#define LOG_INFO(...) (void)0
#endif

#if SPDLOG_ACTIVE_LEVEL <= SPDLOG_LEVEL_WARN
#define LOG_WARN(...) _LOGGER_CALL(spdlog::default_logger_raw(), spdlog::level::warn, __VA_ARGS__)
#else
#define LOG_WARN(...) (void)0
#endif

#if SPDLOG_ACTIVE_LEVEL <= SPDLOG_LEVEL_ERROR
#define LOG_ERROR(...) _LOGGER_CALL(spdlog::default_logger_raw(), spdlog::level::err, __VA_ARGS__)
#else
#define LOG_ERROR(...) (void)0
#endif

#if SPDLOG_ACTIVE_LEVEL <= SPDLOG_LEVEL_CRITICAL
#define LOG_FATAL(...) _LOGGER_CALL(spdlog::default_logger_raw(), spdlog::level::critical, __VA_ARGS__)
#else
#define LOG_FATAL(...) (void)0
#endif

#define LOG_HEXDUMP(lev, buf, len, mfmt, ...)                                                                        \
	do {                                                                                                         \
		auto logger = spdlog::default_logger_raw();                                                          \
		spdlog::level::level_enum lv = static_cast<spdlog::level::level_enum>(lev);                          \
		if (logger->should_log(lv))                                                                          \
		{                                                                                                    \
			spdlog::memory_buf_t mbuf;                                                                   \
			fmt::format_to(std::back_inserter(mbuf), mfmt, ##__VA_ARGS__);                               \
			fmt::format_to(std::back_inserter(mbuf), "{:Xa}", spdlog::to_hex((buf), (buf) + (len), 16)); \
			SPDLOG_LOGGER_CALL(logger, lv, spdlog::string_view_t(mbuf.data(), mbuf.size()));             \
		}                                                                                                    \
	} while (0)

#ifndef LOG_PATTTERN
#define LOG_PATTTERN "%Y-%m-%d %H:%M:%S.%e [%t][%^%5l%$][%s:%# %!] %v"
#endif

#endif

namespace wlog {

constexpr int LOG_MAX_SIZE = 10 * 1024 * 1024;
constexpr int LOG_MAX_FILES = 10;
constexpr int LOG_QUEUE_SIZE = 8192;
constexpr const char* const LOGGER_NAME = "wlog";

// 分别按天、小时、大小分割
constexpr const char* const ROTATION_TYPE_BY_DAY = "by_day";
constexpr const char* const ROTATION_TYPE_BY_HOUR = "by_hour";
constexpr const char* const ROTATION_TYPE_BY_SIZE = "by_size";

constexpr const char* const level_strings[] = {"TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"};

inline std::string level_string(int level)
{
	if (level >= 0 && level <= (int)sizeof(level_strings)) {
		return level_strings[level];
	}

	return "UNKOWN";
}

inline int cast_level(const std::string& level)
{
	for (int i = LOG_LEVEL_TRACE; i <= LOG_LEVEL_FATAL; ++i) {
		if (level == level_strings[i]) {
			return i;
		}
	}
	return std::atoi(level.c_str());
}

struct logger_config {
	std::string name = LOGGER_NAME;
	int level = LOG_LEVEL_INFO;
	int flush_level = LOG_LEVEL_WARN;
	std::string format = LOG_PATTTERN;
	bool disable_stdout = false;
	std::string log_file = "run.log";
	std::string rotation_type = ROTATION_TYPE_BY_DAY;
	int max_files = LOG_MAX_FILES;
	// by size
	int max_size = LOG_MAX_SIZE;
	// 表示按天切割的时刻，具体是时刻通过rotation_hour:rotation_minute指定
	int rotation_hour = 0;
	int rotation_minute = 0;
	// 异步日志线程池大小
	int async_thread_counts = 0;
	int async_queue_size = LOG_QUEUE_SIZE;

	void display() const
	{
		LOG_INFO("name: {}", name);
		LOG_INFO("level: {}({})", level_string(level), level);
		LOG_INFO("flush_level: {}({})", level_string(flush_level), flush_level);
		LOG_INFO("format: {}", format);
		LOG_INFO("disable_stdout: {}", disable_stdout);
		LOG_INFO("log_file: {}", log_file);
		LOG_INFO("rotation_type: {}", rotation_type);
		LOG_INFO("max_files: {}", max_files);
		LOG_INFO("max_size: {}", max_size);
		LOG_INFO("rotation_hour: {}", rotation_hour);
		LOG_INFO("rotation_minute: {}", rotation_minute);
		LOG_INFO("async_thread_counts: {}", async_thread_counts);
		LOG_INFO("async_queue_size: {}", async_queue_size);
	}

	bool check() const
	{
		auto checkEmpty = [](const std::string & str, const std::string & err_msg) {
			if (str.empty()) {
				LOG_ERROR(err_msg);
				return false;
			}
			return true;
		};

		auto checkRange = [](int value, int min_val, int max_val, const std::string & err_msg) {
			if (value < min_val || value > max_val) {
				LOG_ERROR(err_msg, min_val, max_val);
				return false;
			}
			return true;
		};

		auto checkEnum = [](const std::string & str, const std::vector<std::string>& validValues, const std::string & err_msg) {
			for (const auto& validValue : validValues) {
				if (str == validValue) {
					return true;
				}
			}
			LOG_ERROR(err_msg);
			return false;
		};

		static const std::vector<std::string> rotation_types = {
			ROTATION_TYPE_BY_DAY,
			ROTATION_TYPE_BY_HOUR,
			ROTATION_TYPE_BY_SIZE
		};

		if (!checkEmpty(name, "logger name is empty") ||
		    !checkRange(level, LOG_LEVEL_TRACE, LOG_LEVEL_FATAL, "invalid level") ||
		    !checkRange(flush_level, LOG_LEVEL_TRACE, LOG_LEVEL_FATAL, "invalid flush_level") ||
		    !checkEmpty(format, "format is empty") ||
		    !checkEmpty(log_file, "log_file is empty") ||
		    !checkEnum(rotation_type, rotation_types, "invalid rotation_type") ||
		    !checkRange(max_files, 1, 1000, "max_files should be: {} - {}") ||
		    !checkRange(max_size, 1, INT_MAX, "max_size should be: {} - {}") ||
		    !checkRange(rotation_hour, 0, 23, "rotation_hour should be: {} - {}") ||
		    !checkRange(rotation_minute, 0, 59, "rotation_minute should be: {} - {}") ||
		    !checkRange(async_queue_size, 1, INT_MAX, "async_queue_size should be: {} - {}")) {
			return false;
		}

		return true;
	}

	static bool is_true(const std::string& val)
	{
		return (val == "true" || val == "TRUE" || val == "True");
	}

	void set(const std::string& key, const std::string& val)
	{
		typedef void (*ConfigSetter)(logger_config&, const std::string&);
		static const std::unordered_map<std::string, ConfigSetter> config_setters = {
			{
				"name", [](logger_config & conf, const std::string & val)
				{
					conf.name = val;
				}
			},
			{
				"level", [](logger_config & conf, const std::string & val)
				{
					conf.level = cast_level(val);
				}
			},
			{
				"flush_level", [](logger_config & conf, const std::string & val)
				{
					conf.flush_level = cast_level(val);
				}
			},
			{
				"format", [](logger_config & conf, const std::string & val)
				{
					conf.format = val;
				}
			},
			{
				"disable_stdout", [](logger_config & conf, const std::string & val)
				{
					conf.disable_stdout = is_true(val);
				}
			},
			{
				"log_file", [](logger_config & conf, const std::string & val)
				{
					conf.log_file = val;
				}
			},
			{
				"rotation_type", [](logger_config & conf, const std::string & val)
				{
					conf.rotation_type = val;
				}
			},
			{
				"max_files", [](logger_config & conf, const std::string & val)
				{
					conf.max_files = std::atoi(val.c_str());
				}
			},
			{
				"max_size", [](logger_config & conf, const std::string & val)
				{
					conf.max_size = std::atoi(val.c_str());
				}
			},
			{
				"rotation_hour", [](logger_config & conf, const std::string & val)
				{
					conf.rotation_hour = std::atoi(val.c_str());
				}
			},
			{
				"rotation_minute", [](logger_config & conf, const std::string & val)
				{
					conf.rotation_minute = std::atoi(val.c_str());
				}
			},
			{
				"async_thread_counts", [](logger_config & conf, const std::string & val)
				{
					conf.async_thread_counts = std::atoi(val.c_str());
				}
			},
			{
				"async_queue_size", [](logger_config & conf, const std::string & val)
				{
					conf.async_queue_size = std::atoi(val.c_str());
				}
			}
		};

		auto it = config_setters.find(key);
		if (it != config_setters.end()) {
			it->second(*this, val);
		}
		else {
			LOG_ERROR("unknown conf key: {}", key);
		}
	}
};

#ifdef LOG_USE_SPDLOG

inline int get_level()
{
	return static_cast<int>(spdlog::get_level());
}

inline void set_level(int level)
{
	spdlog::set_level(static_cast<spdlog::level::level_enum>(level));
}

inline int init(const logger_config& conf)
{
	conf.display();

	if (!conf.check()) {
		return -1;
	}

	std::vector<spdlog::sink_ptr> sinks;
	std::shared_ptr<spdlog::sinks::sink> rotating_sink;

	if (conf.rotation_type == ROTATION_TYPE_BY_SIZE) {
		rotating_sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(conf.log_file, conf.max_size, conf.max_files);
	}
	else if (conf.rotation_type == ROTATION_TYPE_BY_DAY) {
		rotating_sink = std::make_shared<spdlog::sinks::daily_file_sink_mt>(conf.log_file, conf.rotation_hour, conf.rotation_minute, false, conf.max_files);
	}
	else if (conf.rotation_type == ROTATION_TYPE_BY_HOUR) {
		rotating_sink = std::make_shared<spdlog::sinks::hourly_file_sink_mt>(conf.log_file, false, conf.max_files);
	}
	else {
		LOG_ERROR("invliad rotation_type {}", conf.rotation_type);
		return -1;
	}

	sinks.push_back(rotating_sink);

	if (!conf.disable_stdout) {
		auto stdout_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
		sinks.push_back(stdout_sink);
	}

	std::shared_ptr<spdlog::logger> logger;
	if (conf.async_thread_counts) {
		spdlog::init_thread_pool(conf.async_queue_size, conf.async_thread_counts);
		logger = std::make_shared<spdlog::async_logger>(conf.name, sinks.begin(), sinks.end(), spdlog::thread_pool(), spdlog::async_overflow_policy::block);
	}
	else {
		logger = std::make_shared<spdlog::logger>(conf.name, begin(sinks), end(sinks));
	}

	logger->flush_on(static_cast<spdlog::level::level_enum>(conf.flush_level));

	spdlog::set_default_logger(logger);
	// after set_default_logger
	set_level(conf.level);
	spdlog::set_pattern(conf.format);

	return 0;
}

#endif

inline int init(const std::string& log_file, int level, int max_size = LOG_MAX_SIZE, int max_files = LOG_MAX_FILES,
                int queue_size = LOG_QUEUE_SIZE, int thread_count = 0)
{
	logger_config conf;
	conf.level = level;
	conf.log_file = log_file;
	conf.rotation_type = "by_size";
	conf.max_files = max_files;
	conf.max_size = max_size;
	conf.async_thread_counts = thread_count;
	conf.async_queue_size = queue_size;

	return init(conf);
}

inline int init_from_string(const std::string& conf_str)
{
	logger_config conf;

	std::istringstream input_stream(conf_str);
	std::string line;

	auto trim = [](std::string const & str) {
		std::string::size_type start = str.find_first_not_of(" \t\n\r");
		std::string::size_type end = str.find_last_not_of(" \t\n\r");
		return start != std::string::npos ? str.substr(start, 1 + end - start) : std::string();
	};

	while (std::getline(input_stream, line)) {
		std::istringstream line_stream(line);
		std::string key, val;

		std::getline(line_stream, key, '=');
		std::getline(line_stream, val);

		key = trim(key);
		val = trim(val);

		if (key.empty() || val.empty()) {
			continue;
		}

		conf.set(key, val);
	}

	return init(conf);
}

inline int init_from_file(const std::string& conf_file)
{
	std::ifstream infile(conf_file);
	if (!infile.good()) {
		LOG_ERROR("open conf file {} failed", conf_file);
		return -1;
	}

	std::string content((std::istreambuf_iterator<char>(infile)), std::istreambuf_iterator<char>());
	return init_from_string(content);
}

inline void level_switch_handler(int sig)
{
	int level = get_level();
	if (level >= LOG_LEVEL_WARN)
		level = LOG_LEVEL_TRACE;
	else
		level++;

	if (level < LOG_LEVEL_TRACE)
		level = LOG_LEVEL_TRACE;

	set_level(level);
	LOG_INFO("switch log level to {}", level_string(level));
}

inline void enable_level_switch(int sig = SIGUSR1)
{
	std::signal(sig, level_switch_handler);
}

}

#endif
